package la.iok.hzsvn.lewin.movie.cloud.service;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import la.iok.hzsvn.lewin.movie.cloud.config.RedisKey;
import la.iok.hzsvn.lewin.movie.cloud.entity.OrgUnit;
import la.iok.hzsvn.lewin.movie.cloud.entity.OrgUnitAddress;
import la.iok.hzsvn.lewin.movie.cloud.mapper.OrgUnitMapper;
import la.iok.hzsvn.lewin.movie.cloud.model.OrgUnitGeo;
import la.iok.hzsvn.lewin.movie.cloud.model.OrgUnitParam;
import la.iok.hzsvn.lewin.movie.cloud.model.OrgUnitVo;
import la.iok.hzsvn.lewin.movie.core.RoleEnum;
import la.iok.hzsvn.lewin.movie.core.Token;
import la.iok.hzsvn.lewin.movie.core.exception.ErrorCode;
import la.iok.hzsvn.lewin.movie.core.model.OrgUnitType;
import la.iok.hzsvn.lewin.movie.core.model.SimpleOrgUnitVo;
import la.iok.hzsvn.lewin.mybatis.model.PageVo;
import la.iok.hzsvn.lewin.mybatis.service.BaseServiceImpl;
import la.iok.hzsvn.share.annotations.NotNull;
import la.iok.hzsvn.share.annotations.NotNullElement;
import la.iok.hzsvn.share.annotations.NullElement;
import la.iok.hzsvn.share.annotations.Nullable;
import la.iok.hzsvn.share.collection.IdOperators;
import la.iok.hzsvn.share.tools.exception.SystemException;
import la.iok.hzsvn.share.utils.ArrayUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
public class OrgUnitServiceImpl extends BaseServiceImpl<OrgUnit,OrgUnitMapper,Long> implements OrgUnitService {
    private final OrgUnitAddressService orgUnitAddressService;
    private final RedisTemplate<Object, Object> redisTemplate;
    private final RedisKey redisKey;

    public OrgUnitServiceImpl(OrgUnitMapper orgUnitMapper,
                              OrgUnitAddressService orgUnitAddressService,
                              RedisTemplate<Object, Object> redisTemplate,
                              RedisKey redisKey) {
        super(orgUnitMapper);
        this.orgUnitAddressService = orgUnitAddressService;
        this.redisTemplate = redisTemplate;
        this.redisKey = redisKey;
    }

    @Override
    public Long save(OrgUnitParam orgUnit, Token token) {
        OrgUnitType gt = OrgUnitType.valueOf(orgUnit.getType());
        ErrorCode.ORG_UNIT_TYPE_ERROR.assertNotEquals(OrgUnitType.INVALID,gt,orgUnit.getType());
        String name = ErrorCode.NAME_BLANK_ERROR.assertNotBlank(orgUnit.getName());
        if(orgUnit.getHideCloudMovie() != null){
            ErrorCode.PROPERTY_CHECK_ERROR.assertAnyOf(orgUnit.getHideCloudMovie(), new Integer[]{0,1},"是否隐藏云端剧本","取值只能为0或者1");
        }
        orgUnit.setName(name);
        if(gt == OrgUnitType.FRANCHISEE){
            token.validAnyAuthority(RoleEnum.Admin);
        }else if(gt == OrgUnitType.SHOP){
            token.validAnyAuthority(RoleEnum.Manager);
            OrgUnit o = select(token.getOrgUnitId());
            ErrorCode.ORG_UNIT_NOT_EXIST.assertNotNull(o,token.getOrgUnitId());
            ErrorCode.INVALID_OPERATION.assertTrue(o.franchisee());
        }
        OrgUnit g = new OrgUnit();
        g.setType(orgUnit.getType());
        g.setName(orgUnit.getName());
        g.setHideCloudMovie(orgUnit.getHideCloudMovie());
        g.setCreateUserId(token.getUserId());
        if(!g.franchisee()){
            g.setParentId(token.getOrgUnitId());
        }
        mapper.insertSelective(g);
        // 处理地址
        orgUnitAddressService.save(g.getId(),orgUnit.getAddress());
        addOrgUnitGeo(g.getId());
        return g.getId();
    }

    @Transactional
    @Override
    public int delete(@Nullable Long id) {
        if(id == null){
            return 0;
        }
        OrgUnit child = select("parentId",id,"`id` LIMIT 1");
        ErrorCode.PROPERTY_CHECK_ERROR.assertNull(child,"店铺","存在未删除的店铺,当前记录不能删除");
        orgUnitAddressService.deleteOrgUnitAddress(id);
        return mapper.deleteById(id);
    }

    @Transactional
    @Override
    public void delete(Long id, Token token) {
        validateOrgUnitManagerAuth(id, token);
        delete(id);
    }

    private void validateOrgUnitManagerAuth(Long id, Token token) {
        if(token.hasAuthority(RoleEnum.Admin)){
            //都可以
        }else if(token.hasAuthority(RoleEnum.Manager)){
            //管理员不能删除自己所在的组织
            OrgUnit orgUnit = select(id);
            ErrorCode.ORG_UNIT_NOT_EXIST.assertNotNull(orgUnit, id);
            Set<Long> set = new HashSet<>(childOrgUnitIds(token.getOrgUnitId()));
            ErrorCode.ORG_UNIT_ACCESS_DENIED.assertTrue(set.contains(orgUnit.getId()), orgUnit.getName());
        }else{
            ErrorCode.ORG_UNIT_ACCESS_DENIED.assertTrue(token.hasAuthority(RoleEnum.Admin), id);
        }
    }

    @Override
    @Transactional
    public void update(@NotNull Token token, @Nullable Long orgUnitId, @Nullable OrgUnitParam orgUnitParam) {
        if(orgUnitId == null || orgUnitParam == null){
            return;
        }
        if(token.hasAnyAuthority(RoleEnum.Admin,RoleEnum.Manager)){
            validateOrgUnitManagerAuth(orgUnitId, token);
        }else{
            ErrorCode.ORG_UNIT_ACCESS_DENIED.assertTrue(token.hasAuthority(RoleEnum.ShopManager),orgUnitId);
            ErrorCode.ORG_UNIT_ACCESS_DENIED.assertEqual(orgUnitId,token.getOrgUnitId(),orgUnitId);
        }
        if(orgUnitParam.getHideCloudMovie() != null){
            ErrorCode.PROPERTY_CHECK_ERROR.assertAnyOf(orgUnitParam.getHideCloudMovie(), new Integer[]{0,1},"是否隐藏云端剧本","取值只能为0或者1");
        }
        ErrorCode.ORG_UNIT_NOT_EXIST.assertNotNull(select(orgUnitId), orgUnitId);
        mapper.updateProperty(orgUnitId,"name",ErrorCode.NAME_BLANK_ERROR.assertNotBlank(orgUnitParam.getName()));
        if(orgUnitParam.getHideCloudMovie() != null){ // 名称和这里的属性，应该放到一个sql中更新,暂时先分开更
            mapper.updateProperty(orgUnitId, "hideCloudMovie", orgUnitParam.getHideCloudMovie());
        }
        orgUnitAddressService.save(orgUnitId, orgUnitParam.getAddress());
        addOrgUnitGeo(orgUnitId);
    }

    @Override
    public PageVo<OrgUnitVo> page(int page, int limit, Integer type, String name,Long parentId) {
        Page<OrgUnit> records = PageHelper.startPage(page,limit);
        mapper.list(type,name,parentId);
        Set<Long> orgUnitIds = records.stream().map(OrgUnit::getId).collect(Collectors.toSet());
        List<OrgUnitAddress> list = orgUnitAddressService.list(orgUnitIds);
        Set<Long> parentIds = records.stream().map(OrgUnit::getParentId).collect(Collectors.toSet());
        List<OrgUnit> parents = list(parentIds);
        return page(records,orgUnit -> po2vo(OrgUnitVo.class,orgUnit,list,parents));
    }

    @Override
    public List<OrgUnit> list(@Nullable @NullElement Collection<Long> orgUnitIds) {
        return listInProperty("id",orgUnitIds);
    }

    @Override
    public int count(@Nullable Integer type) {
        if(type == null){
            return count();
        }
        return count("type",type);
    }

    @Override
    public int count(@Nullable Long orgUnitId, @Nullable Integer type) {
        if(orgUnitId == null){
            return count(type);
        }
        List<Long> ids = mapper.orgUnitIds(orgUnitId,type);
        if(ids.contains(orgUnitId)){
            return ids.size();
        }
        if(type == null){
            return ids.size()+1;
        }
        OrgUnit orgUnit = select(orgUnitId);
        if(orgUnit == null){
            return ids.size();
        }
        if(Objects.equals(type,orgUnit.getType())){
            return ids.size()+1;
        }
        return ids.size();
    }

    @Override
    public int count(@NotNull Long parentId, @Nullable Long orgUnitId, @Nullable Integer type) {
        if(Objects.equals(parentId,orgUnitId)){
            return count(orgUnitId,type);
        }
        if(orgUnitId == null){
            return count(parentId,type);
        }
        List<Long> ids = mapper.orgUnitIds(parentId,type);
        if(ids.contains(orgUnitId)){
            return 1;
        }
        return 0;
    }

    @Override
    public List<Long> childOrgUnitIds(@Nullable Long parentId) {
        if(parentId == null){
            return Collections.emptyList();
        }
        return mapper.orgUnitIds(parentId,null);
    }

    @Override
    public List<Long> childOrgUnitIds(Long[] parentIds) {
        return ArrayUtils.runWhenNotEmptyWithTrimNullToSet(parentIds, Collections::emptyList, set->mapper.childrenIds(set));
    }

    @Nullable
    @Override
    public SimpleOrgUnitVo simple(@Nullable Long id) {
        OrgUnit orgUnit = select(id);
        if(orgUnit == null){
            return null;
        }
        return po2SimpleVo(orgUnit);
    }

    private SimpleOrgUnitVo po2SimpleVo(OrgUnit orgUnit) {
        SimpleOrgUnitVo vo = new SimpleOrgUnitVo();
        BeanUtils.copyProperties(orgUnit, vo);
        return vo;
    }

    @Override
    public List<SimpleOrgUnitVo> simple(Long[] ids) {
        if(ids == null || ids.length == 0){
            return Collections.emptyList();
        }
        Set<Long> set = Arrays.stream(ids).filter(Objects::nonNull).collect(Collectors.toSet());
        if(set.isEmpty()){
            return Collections.emptyList();
        }
        List<OrgUnit> list = list(set);
        return list.stream().map(this::po2SimpleVo).collect(Collectors.toList());
    }

    @Override
    public void online(@NotNull Token token, @Nullable Long id) {
        token.validAnyAuthority(RoleEnum.Admin);
        OrgUnit orgUnit = select(id);
        ErrorCode.ORG_UNIT_NOT_EXIST.assertNotNull(orgUnit,id);
        ErrorCode.PROPERTY_CHECK_ERROR.assertTrue(orgUnit.shop(),"类型","只能是店铺");
        update(id,"status",SimpleOrgUnitVo.STATUS_NORMAL);
        addOrgUnitGeo(id);
    }

    @Override
    public void offline(@NotNull Token token, @Nullable Long id) {
        token.validAnyAuthority(RoleEnum.Admin);
        OrgUnit orgUnit = select(id);
        ErrorCode.ORG_UNIT_NOT_EXIST.assertNotNull(orgUnit,id);
        ErrorCode.PROPERTY_CHECK_ERROR.assertTrue(orgUnit.shop(),"类型","只能是店铺");
        update(id,"status",SimpleOrgUnitVo.STATUS_OFFLINE);
        removeOrgUnitGeo(id);
    }

    @Override
    public void buildShopGeo(@NotNull Token token) {
        token.validAnyAuthority(RoleEnum.Admin);
        PageVo<OrgUnitVo> page = page(1,10,OrgUnitType.SHOP.getCode(), null,null);
        redisTemplate.expire(redisKey.shopGeoKey(),0, TimeUnit.MILLISECONDS);
        for(int i = 1; i <= page.getPages(); ++i){
            List<RedisGeoCommands.GeoLocation<Object>> list = page.getRecords().stream().map(orgUnit->{
                RedisGeoCommands.GeoLocation<Object> location = null;
                if(!orgUnit.offlineStatus() && orgUnit.getAddress() != null){
                    location = new RedisGeoCommands.GeoLocation<>(orgUnit.getId(), new Point(orgUnit.getAddress().getLng(), orgUnit.getAddress().getLat()));
                }
                return location;
            }).filter(Objects::nonNull).collect(Collectors.toList());
            redisTemplate.opsForGeo().add(redisKey.shopGeoKey(),list);
        }
    }

    private void addOrgUnitGeo(@NotNull Long orgUnitId){
        OrgUnitAddress address = orgUnitAddressService.orgUnitAddress(orgUnitId);
        if(address != null){
            redisTemplate.opsForGeo().add(redisKey.shopGeoKey(), new Point(address.getLng(), address.getLat()),orgUnitId);
        }
    }

    private void removeOrgUnitGeo(@NotNull Long orgUnitId){
        redisTemplate.opsForGeo().remove(redisKey.shopGeoKey(), orgUnitId);
    }

    @Override
    public List<OrgUnitGeo> geoSearch(Double lng, Double lat, Double radius) {
        ErrorCode.PROPERTY_CHECK_ERROR.assertNotNull(lng, "经度","经度不能为空");
        ErrorCode.PROPERTY_CHECK_ERROR.assertNotNull(lat, "纬度","纬度不能为空");
        ErrorCode.PROPERTY_CHECK_ERROR.assertNotNull(radius, "半径","搜索半径不能为空");
        GeoResults<RedisGeoCommands.GeoLocation<Object>> result = redisTemplate.opsForGeo().radius(redisKey.shopGeoKey(),
                new Circle(new Point(lng, lat), new Distance(radius, RedisGeoCommands.DistanceUnit.METERS)),
                RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().sortAscending());
        if(result == null){
            return Collections.emptyList();
        }
        Map<Long, Integer> map = new HashMap<>();
        List<Long> orgUnitIds = new ArrayList<>();
        result.forEach(item->{
            double distance = item.getDistance().in(RedisGeoCommands.DistanceUnit.METERS).getValue();
            Long orgUnitId = (Long) item.getContent().getName();
            map.put(orgUnitId, (int)distance);
            orgUnitIds.add(orgUnitId);
        });
        List<OrgUnit> list = list(map.keySet());
        List<OrgUnitAddress> addresses = orgUnitAddressService.list(map.keySet());
        List<OrgUnit> parents = list(list.stream().map(OrgUnit::getParentId).collect(Collectors.toSet()));
        return orgUnitIds.stream().distinct().map(orgUnitId->{
            OrgUnit item = IdOperators.find(list, orgUnitId);
            if(item == null){
                return null;
            }
            if(!item.statusNormal()){
                return null;
            }
            OrgUnitGeo vo = po2vo(OrgUnitGeo.class, item,addresses,parents);
            if(vo != null){
                vo.setDistance(map.get(item.getId()));
            }
            return vo;
        }).filter(Objects::nonNull).collect(Collectors.toList());
    }

    @NotNull
    private <T extends OrgUnitVo> T po2vo(Class<T> cls, @NotNull OrgUnit orgUnit,@NotNull @NotNullElement List<OrgUnitAddress> addresses,@NotNull @NotNullElement List<OrgUnit> parents){
        try {
            T vo = cls.getDeclaredConstructor().newInstance();
            BeanUtils.copyProperties(orgUnit,vo);
            addresses.stream().filter(addr -> Objects.equals(addr.getOrgUnitId(), orgUnit.getId())).findFirst()
                    .ifPresent(address -> vo.setAddress(orgUnitAddressService.po2vo(address)));
            OrgUnit o = IdOperators.find(parents,orgUnit.getParentId());
            if(o != null){
                vo.setParentName(o.getName());
            }
            return vo;
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new SystemException("转换对象异常",e);
        }
    }
}
